Skip to content

Agent Loop ​

标签
AI/agent
字数
4191 字
阅读时间
17 分钟

智能体通过一个持续的循环与环境交互,而不是一次性完成任务。这个机制叫 Agent Loop(智能体循环)。

循环结构 ​

感知(Perception)
   ↓
思考(Thought)
   ├── 规划(Planning)
   └── 工具选择(Tool Selection)
   ↓
行动(Action)→ 环境状态变化 → 新的观察(Observation)
   ↑                                    │
   └────────────────────────────────────┘

思考阶段内部再分两步,这是 LLM 智能体与传统智能体在结构上的分界:Planning 基于当前观察和内部记忆更新对任务的理解、制定或调整计划、把复杂目标分解为子任务;Tool Selection 从工具库中选出执行下一步所需的工具并确定调用参数。

行动通过执行器施加到环境,环境产生新的观察回流,构成闭环。

每一轮都在重新组装输入 ​

这一点决定了后面所有失败模式的成因:

每一轮都在重新组装输入

   ┌─────────────────────────────────────────┐
   │ 系统提示                                 │
   │ 相关历史(含之前每一轮的 Thought/Obs)     │
   │ 检索到的记忆                             │
   │ 工具定义                                 │
   └────────────────────┬────────────────────┘
                        │ 拼成一条新的 messages
                        ▼
                 发一次模型请求
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
   「答案」                        「一个动作」
   (纯文本、无工具调用)            (工具名 + 参数)
        │                               │
        ▼                               ▼
   退出循环                     执行动作,把结果追加进上下文
                                        │
                                        └──────▶ 回到组装输入

模型内部没有任何东西跨轮持久化:每一轮都是一次全新的模型请求,
所有连续性都来自运行时选择往上下文里放回去的东西。

模型内部没有任何东西跨轮持久化。 每一轮都是一次全新的模型请求,所有连续性都来自运行时选择往上下文里放回去的东西。所以「智能体记得上一步做过什么」这件事,本质上是运行时把历史重新喂了一遍。

对照 01-智能体核心概念 的环境分类:正因为环境是序贯的,「上一轮的结果」必须显式带进下一轮,循环才有意义。

交互协议:Thought-Action-Observation ​

LLM 驱动这个循环的前提,是把它的输出结构化成固定字段。Thought 与 Action 成对出现:

Thought: 用户想知道北京的天气。我需要调用天气查询工具。
Action: get_weather("北京")

Action 是对外部的指令,由外部**解析器(Parser)**捕获后调用对应函数。函数返回的原始数据(JSON)含冗余信息、格式也不适合模型消费,所以感知系统要把原始输出封装成简洁的自然语言:

Observation: 北京当前天气为晴,气温25摄氏度,微风。

这段文本作为下一轮循环的主要输入。

实现要点 ​

协议靠 prompt 约束,不靠模型自觉。 system_prompt 里必须显式规定:工具清单与签名、输出必须是一对 Thought/Action、Action 必须单行不换行、信息够了必须用 Finish[最终答案] 收尾。

必须截断多余输出。 模型经常一次吐出多组 Thought-Action。原书用这条正则只保留第一组:

python
r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)'

Action 靠正则解析成工具调用。 工具名从 工具名( 提取,参数用 (\w+)="([^"]*)" 抓成 kwargs,再从工具字典分发:

python
tool_name = re.search(r"(\w+)\(", action_str).group(1)
args_str  = re.search(r"\((.*)\)", action_str).group(1)
kwargs    = dict(re.findall(r'(\w+)="([^"]*)"', args_str))

observation = available_tools[tool_name](**kwargs)

这套「prompt 约束格式 → 正则解析 → 字典分发」,就是 LangChain、LlamaIndex 这类框架早期的设计内核。更可靠的做法是 Function Calling——由模型原生输出结构化调用,不再依赖正则去猜(见 09-工具系统与 Function Calling)。

解析失败要变成观察,而不是崩溃。 拿不到 Action 时,把错误信息(「未能解析到 Action 字段,请确保回复严格遵循 Thought: ... Action: ... 格式」)作为一条 Observation 塞回历史,让模型在下一轮自行修正——错误信息本身也是循环的输入。

循环必须有上限。 原书示例设为 5 次。没有上限,智能体可能反复调用同一个工具,或者一直思考而不收敛。下面单独说这件事。

循环怎么停 ​

原书只用了 max_steps 加 Finish 两个机制,生产环境需要三类停止条件同时具备。

正常收敛 ​

模型返回一个纯文本回复且不含工具调用——运行时据此退出循环,把这个回复作为答案返回。这是最常见的正常出口。

硬上限(三道,各抓不同的失败模式) ​

上限抓什么建议取值
最大迭代数死循环简单问答型 10;研究型 25;编码型 50–100
Token 预算昂贵循环(轮数少但每次带巨量上下文)平均成功运行的 10 倍(典型任务 5k tokens → 预算 50k)
墙钟超时卡住的工具调用(网络挂起)p95 任务完成时间的 2–3 倍

三道缺一不可,因为它们各自漏掉一类:

  • 只设迭代数:一个每轮都读 5 万 token 文件的 agent,3 轮就爆预算,但迭代数 25 拦不住它
  • 只设 token 预算:一个做 100 次小快调用的 agent 永远碰不到预算,但迭代数能拦
  • 只设超时:一个单次工具调用挂几分钟的场景,只有超时能拦

取值原则:宽松到正常任务永远碰不到,紧到失控能在几秒内而不是几分钟内被抓住。如果最难的任务需要 15 轮,把上限设到 25;一旦跑到 25 轮,说明已经出问题了,继续跑不会修复它。这个上限不是性能优化,是安全机制。

命中上限时要优雅降级,而不是抛错:把已完成的进度总结一下返回。研究 agent 找到 10 篇里的 7 篇,比返回「Error: iteration limit exceeded」有用得多。

python
if iteration >= MAX_ITERATIONS:
    summary = summarize_progress(messages)
    return f"I reached my iteration limit. Here is what I found so far: {summary}"

还要记录是哪一道上限被触发的——这个数据能区分「该调上限」还是「有底层问题要修」。

三道硬上限各自拦住的形态不同。

迭代数上限(抓死循环)
   轮数   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ 25 轮处被拦
                                    └─ 每轮开销很小的 agent 也会碰到它

Token 预算(抓昂贵循环:轮数少但每次带巨量上下文)
   Token  ▓▓▓│                             平均成功运行的 10 倍处被拦
        └─ 每轮读 5 万 token 的 agent,轮数远没到 25,预算已经爆了
        └─ 反过来:100 次小快调用永远碰不到预算,要靠迭代数拦

墙钟超时(抓卡住的工具调用)
   时间   ──────────────────────────────│ p95 × 2–3 处被拦
                                        └─ 一个工具调用挂几分钟,
                                           前两道都拦不住(轮数与 token 都没超)

取值原则:宽松到正常任务永远碰不到,紧到失控能在几秒内、而不是几分钟内被抓住。
         最难的任务需要 15 轮就把上限设到 25;跑到 25 轮说明已经出问题了,
         继续跑不会修复它。这不是性能优化,是安全机制。

命中之后要优雅降级:把已完成的进度总结一下返回 ——
找到 10 篇里的 7 篇,比返回 「Error: iteration limit exceeded」有用得多。
同时记录是哪一道上限被触发,这个数据能区分「该调上限」还是「有底层问题要修」。

外部信号 ​

人工审批检查点:碰到破坏性操作(删文件、改配置、发邮件、动数据库、计费)时暂停等确认。

错误阈值:连续失败达到一定次数就直接放弃。这条针对的是「工具一直报错、模型不知道怎么解」的情形。

重复动作检测:连续若干轮出现相同动作得到相同观察时中止并换方向。有些实现用更激进的判据——同一工具、同一参数被反复调用就直接断。

五个失败模式 ​

循环在概念上简单,在生产里意外地难。下面这些是大多数实现会踩的:

失败模式表现护栏
无限循环模型反复用同一组参数调同一个工具,因为工具一直返回它不知道怎么处理的错误硬性迭代上限 + 检测连续相同的工具调用提前中断
上下文爆炸每轮都追加 token,20 轮啰嗦的工具结果之后窗口填满,模型丢掉前面的步骤摘要或截断旧工具结果;用滑动窗口或外部记忆
过早退出模型在第 1 轮之后就给出语气自信的最终答案,跳过了验证步骤系统提示里写明「回答前必须再用一次工具调用验证」
幻觉工具调用编出不存在的工具名,或参数形状不对,导致运行时错误执行前按 schema 校验参数;返回结构化错误让模型能自纠
成本静默爆炸预计 3 轮的任务跑了 30 轮,烧掉 10 倍预算记录每会话的轮数与 token 消耗;迭代上限之外再加成本预算

「幻觉工具调用」这一条值得强调:模型的错误在于把「记不清的接口」当成「记得的接口」写出来,而不是内容本身出错(参见 07-模型幻觉)。护栏的关键是返回结构化错误而不是抛异常——错误信息进上下文,模型下一轮才有机会改对。

上下文的增长与错误累积 ​

增长的量级 ​

每轮的 Thought + Action + Observation 是一个三元组。每个三元组平均约 200 tokens,十轮下来约 2000 tokens;再叠加系统提示与工具定义,总量就会逼近甚至超过窗口上限,导致 mid-session forgetting(会话中途遗忘)。

即使没到硬上限,性能也已经在退化:长上下文会让每次调用更慢,而且模型会丢失埋在长对话中间的信息——这就是 lost in the middle 效应。 一个 100K tokens 的上下文如果 90% 是早期轮次的工具结果,模型在为它已经不需要的信息支付注意力成本。

三种处理策略 ​

策略做法风险
截断超过阈值就丢掉最老的消息简单有效,但会忘掉仍然相关的早期工具结果
摘要(摘要压缩)定期让模型总结到目前为止的对话,用摘要替换完整历史能把 10k tokens 压到 500,但摘要可能丢关键细节
滑动窗口保留系统提示 + 最前 N 条 + 最后 M 条,丢掉中间保住原始任务与最近上下文,总长度可控

这三种都指向同一件事:循环的第 N 轮不是免费的。这也是 19-上下文工程的工程实践 里 Compaction 那套机制的动机来源——长时程任务的三种手段(压缩整合 / 结构化笔记 / 子代理架构)正是为这个增长问题准备的。

还有一条分阶段策略:规划阶段给全量上下文,执行每个步骤时切到短上下文。

三种处理策略作用在同一条消息序列上,各自丢掉的部分不同。

原始序列(系统提示 + 逐轮累积的三元组)
[SYS][T1][T2][T3][T4][T5][T6][T7][T8][T9][T10][T11][T12]
      └─ 每个三元组(Thought + Action + Observation)平均约 200 tokens
         十轮约 2000 tokens,再叠加系统提示与工具定义就逼近窗口上限

截断:超过阈值就丢掉最老的消息
[SYS][                    ][T9][T10][T11][T12]
      └─ 简单有效,但丢掉的早期工具结果可能仍然相关

摘要:定期让模型总结到目前为止的对话,用摘要替换完整历史
[SYS][ 摘要 ≈ 500 token                   ][T11][T12]
      └─ 能把 10k tokens 压到 500,代价是摘要可能丢掉关键细节

滑动窗口:保留系统提示 + 最前 N 条 + 最后 M 条
[SYS][T1][T2][                               ][T11][T12]
      └─ 保住原始任务与最近上下文,总长度可控,中间整段丢弃

三者指向同一件事:循环的第 N 轮不是免费的。

错误累积的算术 ​

单次模型调用的错误率哪怕只有几个百分点,多轮之后也会被放大。有文章给出 LLM 幻觉率在 5%–15% 区间的说法——按这个量级估算,十轮循环的累积错误概率就接近 100%,所以每轮的验证机制不是锦上添花。

「5%–15%」这个区间来自技术博客,未追到原始出处与测量条件(不同任务、不同模型的差异会很大)。标为待验证。它的用途是提示**量级**,不要当作精确指标引用。

结论的方向是确定的:光靠更强的一次性推理解决不了长任务,靠的是循环设计本身的韧性——把不可靠的单次调用放进一个有护栏、有验证、有状态管理的循环里。

一个完整的三轮执行 ​

输入:「查询今天北京的天气,然后根据天气推荐一个旅游景点」

轮次Thought 要点ActionObservation
1先拿天气,再据此推荐景点get_weather(city="北京")北京当前天气:Sunny,气温 26 摄氏度
2已知晴天且温度适中,据此筛选get_attraction(city="北京", weather="Sunny")颐和园(湖景 + 古建筑)、长城
3信息够了,可以作答Finish[最终答案]任务完成

这条链路演示了四件事:任务分解、工具调用、上下文理解、结果合成。

三轮执行连起来看,循环里每一轮的输入都是重新拼出来的。

这条链路演示了四件事:任务分解、工具调用、上下文理解、结果合成。

相关 ​

参考 ​

贡献者 ​

文件历史 ​